// umoznit Klein {<Otto Klein} {<Viliam Klein}
// ALT+K aby oznacilo jedno predch  slovo ako explicit keyword. ak stlacene hned potom znova, tak 2 prech. slova, atd.

// AppTab* GetAppTab(int index)
// CheckAppTabType() ak sa da nejako

// odjebat cele Connect / Disconnect?
// zaloha nastavitelna v Settings, automaticky pri ukonceni

// premenovanie keywordov (prerobit na callbackovacie OnReparseAllNotes)
// mazat note z dialogu kde je otvoreny
// ked sa zada vyhladavanie podla datumu a nic sa nenajde, ukazat tlacitko ktore umozni
//    vytvorit poznamku s tym datumom

// insert keyword dialog, odjebavat medzery na konci

// Jozef Zahradnicek  -> Zahradnick*
// Pavol, Pavel -> Pavl*
// Jan Paulin[y]-Toth[]
//   - kozlowski -> kozlowsk*
// Kamenec -> Kamenc*

//   - umoznit  Slovo {<Keyw1} {<Keyw2}
//     - co v pripade  Slovo1 Slovo2 {<<Keyw1} {<Keyw2} - zakazat, alebo asociovat na cely najvacsi rozsah?
//     - zakazat 2x to iste?

// BUG:
//   - restornut original config ked niekto v SettingsDialog klikne cancel
//   - jebne dialog pri ALT+M ked ho clovek zavrie?

// TODO:
//   - V keyword liste prefix "-" exclude veci
//   - (idea) Lockovanie Tabov, nech sa nezavru cez CTRL+W - pre klucove taby k zdroju ktory
//     spracuvem

#define APP_MIN_SIZE_X 640
#define APP_MIN_SIZE_Y 480

#include <wx/wx.h>
#include <wx/ffile.h>
#include <wx/stdpaths.h>
#include <limits.h>

#include <wx/aui/auibook.h>

#include "util.h"
#include "app_frame.h"
#include "notes_panel.h"
#include "edits_panel.h"
#include "keywords_panel.h"
#include "note_dialog.h"
#include "filter_dialog.h"
#include "settings.h"
#include "settings_dialog.h"
#include "debug.h"
#include "keyword_dialog.h"
#include "keyword_filter.h"

#include "dbase.h"
#include "db_sqlite3.h"

// needed for process creation
#ifdef TARGET_WIN32
  #define WIN32_LEAN_AND_MEAN
//  #include <windows.h>
#endif


using std::vector;

class MyApp: public wxApp
{
    AppFrame *frame;
    virtual bool OnInit();
    virtual int OnExit();
};
IMPLEMENT_APP(MyApp)

enum
{
    ID_Quit = 1000,

    // File
    ID_Connect,
    ID_Disconnect,
    ID_About,
    ID_Upload,
    ID_Download,
    ID_Export,
    ID_ToggleFullScreen,
    ID_Settings,

    // Note
    ID_NewNote,
    ID_FindNotes,
    ID_EditFilter,
    ID_DateFilter,
    ID_EditDateFilter,
    ID_EditedNotes,
    ID_EditLastNote,
    ID_ReparseNotes,
    ID_Source,

    // Keyword
    ID_NewKeyword,
    ID_FindKeywords,
    ID_UnknownKeywords,

    // Tab
    ID_NextTab,
    ID_PrevTab,
    ID_CloseTab,
    ID_CloseAllTabs
};

BEGIN_EVENT_TABLE(AppFrame, wxFrame)
    EVT_CLOSE(AppFrame::OnClose)

    // File
    EVT_MENU(ID_Connect, AppFrame::OnConnectDB)
    EVT_MENU(ID_Download, AppFrame::OnDownload)
    EVT_MENU(ID_Upload, AppFrame::OnUpload)
    EVT_MENU(ID_Disconnect, AppFrame::OnDisconnectDB)
    EVT_MENU(ID_Export, AppFrame::OnExport)
    EVT_MENU(ID_ToggleFullScreen, AppFrame::OnToggleFullScreen)
    EVT_MENU(ID_Settings, AppFrame::OnSettings)
    EVT_MENU(ID_About, AppFrame::OnAbout)
    EVT_MENU(ID_Quit, AppFrame::OnQuit)

    // Note
    EVT_MENU(ID_NewNote, AppFrame::OnNewNote)
    EVT_MENU(ID_FindNotes, AppFrame::OnFindNotes)
    EVT_MENU(ID_EditFilter, AppFrame::OnEditFilter)
    EVT_MENU(ID_DateFilter, AppFrame::OnDateFilter)
    EVT_MENU(ID_EditDateFilter, AppFrame::OnEditDateFilter)
    EVT_MENU(ID_EditedNotes, AppFrame::OnEditedNotes)
    EVT_MENU(ID_EditLastNote, AppFrame::OnEditLastNote)
    EVT_MENU(ID_ReparseNotes, AppFrame::OnReparseAllNotes)
    EVT_MENU(ID_Source, AppFrame::OnSetSource)

    // Keyword
    EVT_MENU(ID_NewKeyword, AppFrame::OnNewKeyword)
    EVT_MENU(ID_FindKeywords, AppFrame::OnFindKeywords)

    // Tab
    EVT_MENU(ID_NextTab,  AppFrame::OnNextTab)
    EVT_MENU(ID_PrevTab,  AppFrame::OnPrevTab)
    EVT_MENU(ID_CloseTab, AppFrame::OnCloseTab)
    EVT_MENU(ID_CloseAllTabs, AppFrame::OnCloseAllTabs)
    EVT_AUINOTEBOOK_PAGE_CHANGED(wxID_ANY, AppFrame::OnTabChanged)
    EVT_AUINOTEBOOK_PAGE_CLOSE(wxID_ANY, AppFrame::OnTabClosed)
    EVT_AUINOTEBOOK_DRAG_DONE(wxID_ANY, AppFrame::OnTabDragDone)

END_EVENT_TABLE()

int MyApp::OnExit()
{
    config->Save();
    InstanceWatcherCheck();
    return 0;
}

bool MyApp::OnInit()
{
    init_log("log.txt");
    dbg("OnInit\n");

    // config
    config = new Config("config");
    config->LoadLang();
    wxString lang = config->lang;
    if (lang.IsEmpty()) {
        lang="en_US";
        config->lang = lang;
    }

    // use selected locale (or fallback to english)
    static wxLocale locale;
    const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(lang);
    if (!langinfo) {
        ErrMsg(_("Unknown language '%s'.\n"
                 "Defaulting to English.\n"
                 "Use File->Settings to choose different language."),
               config->lang);
        goto lang_failed;
    }
    if (!wxLocale::IsAvailable(langinfo->Language)) {
        ErrMsg(_("Language '%s' (%s) not available.\n"
                 "Defaulting to English.\n"
                 "Use File->Settings to choose different language."),
              lang, langinfo->Description);
        goto lang_failed;
    }
    if (!locale.Init(langinfo->Language)) {
        // Init() can fail because wxWidgets catalog for specified language is not found.
        // We can safely ignore that.
    }
    locale.AddCatalogLookupPathPrefix("lang/");
    if (!locale.AddCatalog(langinfo->CanonicalName + ".mo")) {
        ErrMsg(_("Catalog 'lang/%s.mo' for language '%s' (%s) not found\n"
                 "Defaulting to English.\n"
                 "Use File->Settings to choose different language."),
               langinfo->CanonicalName, lang, langinfo->Description);
        goto lang_failed;
    }
    dbg("Loaded language '%s' (%s)\n", lang, langinfo->Description);
lang_failed:

    // now that we have translations loaded, finish loading config
    if (!config->Load()) {
        ErrMsg(_("Failed to load config ('%s' at '%s').\n"
                 "Falling back to default configuration.\n"
                 "Use File->Settings to customize the application."),
               "config", wxStandardPaths::Get().GetExecutablePath());
    }


    frame = new AppFrame(_("History NoTes"), wxPoint(50, 50), wxSize(400, 600));
    frame->Show(true);
    SetTopWindow(frame);

    return true;
}


AppFrame::AppFrame(
            const wxString& title,
            const wxPoint& pos,
            const wxSize& size
            )
    : wxFrame(NULL, -1, title, pos, size)
{
    // icons
    wxImage::AddHandler(new wxICOHandler());
    wxIconBundle icons("AppIcon.ico", wxBITMAP_TYPE_ICO);
    SetIcons(icons);

    fullscreen = false;
    status_bar = this->CreateStatusBar();

    wxMenu *menuFile = new wxMenu;
    menuFile->Append( ID_Download,   _("&Download") );
    menuFile->Append( ID_Connect,    _("&Connect") );
    menuFile->Append( ID_Disconnect, _("D&isconnect") );
    menuFile->Append( ID_Upload,     _("&Upload") );
    menuFile->Append( ID_Export,     _("&Export") );
    menuFile->Append( ID_ToggleFullScreen, _("Toggle &fullscreen\tF11") );
    menuFile->AppendSeparator();
    menuFile->Append( ID_Settings,   _("&Settings") );
    menuFile->Append( ID_About,      _("About") );
    menuFile->Append( ID_Quit,       _("E&xit") );

    wxMenu *menuNote = new wxMenu;
    menuNote->Append( ID_FindNotes,      _("&Find\tCTRL-F"));
    menuNote->Append( ID_EditFilter,     _("Edit Fil&ter\tCTRL-SHIFT-F"));
    menuNote->Append( ID_DateFilter,     _("&Date Filter\tCTRL-D"));
    menuNote->Append( ID_EditDateFilter, _("&Edit Date Filter\tCTRL-SHIFT-D"));
    menuNote->Append( ID_NewNote,        _("&New\tCTRL-N"));
    menuNote->Append( ID_EditedNotes,    _("&Edited\tCTRL-E"));
    menuNote->Append( ID_EditLastNote,   _("&Last\tCTRL-L"));
    menuNote->Append( ID_ReparseNotes,   _("&Reparse notes"));
    menuNote->Append( ID_Source,         _("&Source\tCTRL-S"));

    wxMenu *menuKeyword = new wxMenu;
    menuKeyword->Append( ID_NewKeyword,      _("&Add\tCTRL-A"));
    menuKeyword->Append( ID_FindKeywords,    _("&Find\tCTRL-K"));
    menuKeyword->Append( ID_UnknownKeywords, _("&Uknown"));

    wxMenu *menuTab = new wxMenu;
    menuTab->Append( ID_NextTab,      _("&Next\tCTRL-PageDown"));
    menuTab->Append( ID_PrevTab,      _("&Prev\tCTRL-PageUp"));
    menuTab->Append( ID_CloseTab,     _("&Close\tCTRL-W"));
    menuTab->Append( ID_CloseAllTabs, _("Close &All"));

    wxMenuBar *menuBar = new wxMenuBar;
    menuBar->Append( menuFile,    _("&File"));
    menuBar->Append( menuNote,    _("&Note"));
    menuBar->Append( menuKeyword, _("Key&word"));   // ALT+K conflicts with Insert keyword in NoteDialog
    menuBar->Append( menuTab,     _("&Tab"));
    SetMenuBar(menuBar );

    notebook = new wxAuiNotebook(this, -1);

    // TODO: for wxAUI_NB_TAB_MOVE, we need to get rid of 'tabs',
    // because it's quite hard to keep it in sync with dragging
    // getting rid of it should be good idea anyway
    notebook->SetWindowStyleFlag(wxAUI_NB_SCROLL_BUTTONS | wxAUI_NB_CLOSE_ON_ACTIVE_TAB);

    notebook->SetArtProvider(new wxAuiDefaultTabArt());

    source = "";
    new_note_counter = 1;
    new_keyword_counter = 1;

    // database
    db = new DBsqlite3();
    db_downloaded = 0;
    ConnectDB();

    SetSizeHints(APP_MIN_SIZE_X, APP_MIN_SIZE_Y);
    Maximize(true);
    this->Fit();
}

void AppFrame::OnToggleFullScreen(wxCommandEvent& event)
{
    dbgenter("AppFrame::OnToggleFullscreen(%s)\n", fullscreen?"off":"on");
    fullscreen = !fullscreen;
    ShowFullScreen(fullscreen);
}

void AppFrame::OnExport(wxCommandEvent& event)
{
    dbgenter("AppFrame::OnExport()\n")
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    wxString filename;
    if (!InputString(&filename, _("Destination file name"), _("Export")))
        return;

    NoteFilter filter;
    if (!GetNoteFilter(_("Export notes filter"), &filter))
        return;
    filter.max_notes = LONG_MAX;
    dbg("filter = %s\n", filter.ToString());

    vector<Note> export_notes;
    if (!db->SelectByFilter(&export_notes, filter))
        return;
    dbg("read %d notes\n", export_notes.size());

    wxFFile f;
    if (!f.Open(filename, "wt")) {
        ErrMsg(_("Failed to open '%s' for writing:"), filename);
        return;
    }

    for (int i = 0; i < (int)export_notes.size(); i++) {
        f.Write(export_notes[i].FormatForDisplay(), wxMBConvUTF8());
        f.Write("\n\n---------------------------------\n");
    }

    f.Close();
}

void AppFrame::OnClose(wxCloseEvent& event)
{
    dbgenter("OnClose()\n")
    if (db->isConnected()) {
/*        if (!event.CanVeto()) {
            dbg("DB not disconnected, but can't veto the close event\n");
        }
        else {
            if (db->isModified()) {
                if (!Confirm(_("Database is still connected and changes weren't uploaded.\nDisconnect & force exit without uploading?"))) {
                    event.Veto();
                    return;
                }
            }

        }
*/
        db->Disconnect();
    }
/*    else if (db->isModified()) {
        if (!event.CanVeto()) {
            dbg("DB not uploaded, but can't veto the close event\n");
        }
        else if (!Confirm(_("Database was changed, but not uploaded. Force exit anyway?"))) {
            event.Veto();
            return;
        }
    }
*/
    dbg("close");
    close_log();

    // let the default handler destroy window
    event.Skip();
}

bool AppFrame::DownloadFile(const wxString& filename, const wxString& password)
{
    dbgenter("DownloadFile(filename='%s', password='nbusr123')\n", filename);
#ifdef TARGET_WIN32
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    wxString command = wxString::Format("pscp.exe -pw %s fasmlib.x86asm.net@x86asm.net:/%s %s", password, filename, filename);
    int handle=CreateProcessW(L"pscp.exe", command.wchar_str(), 0, 0, TRUE, 0, 0, 0, &si, &pi);
    if (!handle) {
        TCHAR *winmsg;
        if (0 == FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, 0, GetLastError(),
                               MAKELANGID(LANG_NEUTRAL,SUBLANG_NEUTRAL), (TCHAR*)&winmsg, 1, 0)) {
            ErrMsg(_("Failed to launch pscp.exe, failed to get error message"));
        } else {
            wxString wxmsg(_("Failed to execute pscp.exe: "));
            wxmsg += wxString(winmsg);
            ErrMsg(wxmsg);
            LocalFree(winmsg);
        }
        return false;
    }

    DWORD exitcode;
    WaitForSingleObject(pi.hProcess, INFINITE );
    GetExitCodeProcess(pi.hProcess, &exitcode);
    if (exitcode == 0) {
        dbg("Done\n");
        db_downloaded=1;
    } else {
        dbg("Exit code = %d\n", (int)exitcode);
        wxString wxmsg = wxString::Format(_("pscp.exe failed, exit code %d, see console for error message"), exitcode);
        ErrMsg(wxmsg);
        return false;
    }
#else
    ErrMsg(_("Download not supported yet on this platform"));
    return false;
#endif
    return true;
}

bool AppFrame::UploadFile(const wxString& filename, const wxString& password)
{
    dbgenter("UploadFile(filename='%s', password='nbusr123')\n", filename);
#ifdef TARGET_WIN32
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    wxString command = wxString::Format("pscp.exe -pw %s %s fasmlib.x86asm.net@x86asm.net:", password, filename);
    int handle=CreateProcess( _T("pscp.exe"), command.wchar_str(), 0, 0, TRUE, 0, 0, 0, &si, &pi);
    if (!handle) {
        TCHAR *winmsg;
        if (0 == FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, 0, GetLastError(), MAKELANGID(LANG_NEUTRAL,SUBLANG_NEUTRAL), (TCHAR*)&winmsg, 1, 0)) {
            ErrMsg(_("Failed to launch pscp.exe, failed to get error message"));
        } else {
            wxString wxmsg(_("Failed to execute pscp.exe: "));
            wxmsg += wxString(winmsg);
            ErrMsg(wxmsg);
            LocalFree(winmsg);
        }
        return false;
    }

    DWORD exitcode;
    WaitForSingleObject(pi.hProcess, INFINITE);
    GetExitCodeProcess(pi.hProcess, &exitcode);
    if (exitcode != 0) {
        dbg("Exit code = %d\n", (int)exitcode);
        wxString wxmsg = wxString::Format(_("pscp.exe failed, exit code %d, see console for error message"), exitcode);
        ErrMsg(wxmsg);
        return false;
    }
#else
    ErrMsg(_("Upload not supported yet on this platform"));
    return false;
#endif
    return true;
}

void AppFrame::OnUpload(wxCommandEvent& WXUNUSED(event))
{
    dbgenter("OnUpload()\n");
    if (db->isConnected()) {
        ErrMsg(_("Database must be disconnected for uploading"));
        return;
    }
    if (!db->isModified()) {
        if (!Confirm(_("Database wasn't modified in this session.\nUpload anyway?")))
            return;
    }
    wxString password;
    if (InputString(&password, _("Enter password: "), _("Password"))) {
        if (!UploadFile("subor.db", password))
            return;
        if (!UploadFile("mam.ods", password))
            return;
    }
    db->setModified(false);
}

void AppFrame::OnDownload(wxCommandEvent& WXUNUSED(event))
{
    dbgenter("OnDownload()\n");
    // TODO: connect
    if (db->isConnected()) {
        ErrMsg(_("Database must be disconnected for uploading"));
        return;
    }
    if (db_downloaded && db->isModified()) {
        if (!Confirm(_("Database was changed in this session.\nDownload anyway?")))
            return;
    }
    wxString password;
    if (InputString(&password, _("Enter password: "), _("Password"))) {
        if (!DownloadFile("subor.db", password))
            return;
        if (!DownloadFile("mam.ods", password))
            return;
    }
}

void AppFrame::OnQuit(wxCommandEvent& event)
{
    dbgenter("OnQuit()\n");
    Close(false);
}

void AppFrame::OnAbout(wxCommandEvent& event)
{
    dbgenter("OnAbout()\n");
    wxString message = wxFormat(_("History NoTes\n\nStarted 7.feb.2010\n     by vid\nwx-%u.%u.%u"),
            wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER);
    wxMessageBox( message, _("History NoTes"), wxOK | wxICON_INFORMATION, this );
}

void AppFrame::OnNewNote(wxCommandEvent& event)
{
    dbgenter("OnNewNote()\n");
    NewNoteDialog();
}

wxString AppFrame::CaptionNewNote() const
{
    return wxString::Format(_("New note %d"), new_note_counter);
}

wxString AppFrame::CaptionFromFilter(const NoteFilter& filter) const
{
    // format caption of new tab
    wxString caption;
    if (!filter.from_date.empty() && !filter.to_date.empty()) {
        if (filter.keywords.empty())
            caption = wxFormat(_("%s to %s"), filter.from_date, filter.to_date);
        else
            caption = wxFormat(_("%s (%s to %s)"), filter.KeywordsToString(), filter.from_date, filter.to_date);
    } else if (!filter.from_date.empty())
        caption = wxFormat(_("%s (from %s)"), filter.KeywordsToString(), filter.from_date);
    else if (!filter.to_date.empty())
        caption = wxFormat(_("%s (to %s)"), filter.KeywordsToString(), filter.to_date);
    else if (!filter.keywords.empty())
        caption = filter.KeywordsToString();
    else
        caption = _("All notes");

    if (!filter.body.IsEmpty())
        caption += wxFormat(" {%s}", filter.body);

    return caption;
}

void AppFrame::AddNotesPanel(const NoteFilter& filter)
{
    dbgenter("AddNotesPanel(%s)\n", filter.ToString());

    // create new panel
    NotesPanel *panel = new NotesPanel(this, notebook, db, &source);

    // add it to tab
    dbg("-adding tab\n");
    wxString name = CaptionFromFilter(filter);
    notebook->AddPage(panel, name, true);

    // set filter on it
    panel->SetFilter(filter);

    // display number of notes in panel
    wxString status_msg = wxString::Format(_("%u notes"), panel->GetItemCount());
    SetStatusText(status_msg);
}

void AppFrame::AddEditsPanel(const NoteFilter& filter)
{
    dbgenter("AddEditsPanel()\n");

    // create new panel
    EditsPanel *panel = new EditsPanel(this, notebook);

    // add it to tab
    wxString name = _("Edited notes");
    notebook->AddPage(panel, name, true);

    // set current filter
    panel->SetFilter(filter);

}

void AppFrame::NewKeywordDialog()
{
    dbgenter("AppFrame::NewKeywordDialog()\n");

    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    // create keyword dialog
    KeywordDialog *keyword_dialog = new KeywordDialog(notebook, this, Keyword());
    keyword_dialog->SetFocus();

    // add it as tab
    notebook->AddPage(keyword_dialog, wxFormat(_("New keyword %u"), new_keyword_counter), true);

}

void AppFrame::NewNoteDialog()
{
    dbgenter("AppFrame::NewNoteDialog()\n");
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    // create note dialog
    NoteDialog *note_dialog = new NoteDialog(notebook, this, Note());
    note_dialog->SetFocus();

    // add it as tab
    notebook->AddPage(note_dialog, CaptionNewNote(), true);

}

void AppFrame::EditNoteDialog(const Note& note)
{
    dbgenter("AppFrame::EditNoteDialog()\n");
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    dbg("Edit note, ID=%d\n", note.id);

    // if note is already being edited, just focus that tab
    for (int i = 0; i < (int)notebook->GetPageCount(); i++) {
        if (NoteDialog* dlg = dynamic_cast<NoteDialog*>( notebook->GetPage(i) )) {
            if (dlg->getID() == note.id) {
                notebook->SetSelection(i);
                dbg("Already opened in tab %d", i);
                return;
            }
        }
    }

    // create note dialog
    NoteDialog *note_dialog = new NoteDialog(notebook, this, note);
    note_dialog->SetFocus();

    // set caption
    wxString final_caption = wxFormat(_("Edit note #%u"), note.id);

    // add it as tab
    notebook->AddPage(note_dialog, final_caption, true);

}

void AppFrame::OnFindNotes(wxCommandEvent& event)
{
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    NoteFilter filter;
    if (!GetNoteFilter(_("Find notes filter"), &filter))
        return;

    AddNotesPanel(filter);
}

void AppFrame::OnEditedNotes(wxCommandEvent& event)
{
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    NoteFilter filter;
    if (!GetNoteFilter(_("Edited notes filter"), &filter))
        return;

    AddEditsPanel(filter);
}

void AppFrame::OnEditLastNote(wxCommandEvent& event)
{
    if (edited_notes.size() == 0) {
        wxBell();
        return;
    }
    EditNoteDialog(edited_notes[0]);
}

void AppFrame::OnSetSource(wxCommandEvent& event)
{
    InputString(&source, _("Source:"), _("Set Source name"));
}

void AppFrame::OnDateFilter(wxCommandEvent& event)
{
    dbgenter("AppFrame::OnDateFilter()\n");

    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    NoteFilter filter;
    if (!GetNoteFilter(_("Date Filter"), &filter, FILTERDIALOG_DATE))
        return;

    AddNotesPanel(filter);

}

void AppFrame::OnReparseAllNotes(wxCommandEvent& event)
{
    // TODO
    ErrMsg("unimplemented!");
}

void AppFrame::DisplayStatusInfo()
{
    int note_count;
    if (!db->GetNoteCount(&note_count))
        Close(true);
    int keyword_count = keyword_list.KeywordCount();
    int hint_count = keyword_list.HintVariantsCount();
    wxString msg = wxFormat("%d notes, %d keywords, %d keyword hints", note_count, keyword_count, hint_count);
    SetStatusText(msg);
}

void AppFrame::ConnectDB()
{
    dbg("Connect()\n");

/*
#ifndef TARGET_LINUX  //not implemented yet
    if (!db_downloaded) {
        if (!Confirm(_("Database wasn't downloaded. Edit local copy?"), _("Warning")))
            return;
    }
#endif
*/
    if (!db->Connect("subor.db"))
        return;
    dbg("Connected\n");
    if (!LoadKeywords())
        Close(true);

    DisplayStatusInfo();
}

void AppFrame::OnConnectDB(wxCommandEvent& event)
{
    ConnectDB();
}

void AppFrame::OnDisconnectDB(wxCommandEvent& event)
{
    // TODO: Check if there are some messages modified

    // force close all tabs
    while (notebook->GetPageCount() > 0)
        CloseTab(0, true);

    // TODO: unload keywords

    DisplayStatusInfo();

    // disconnect databaze
    db->Disconnect();
}

void AppFrame::OnNextTab(wxCommandEvent& event)
{
    NextTab();
}

void AppFrame::NextTab()
{
    notebook->AdvanceSelection(true);
}

void AppFrame::OnPrevTab(wxCommandEvent& event)
{
    PrevTab();
}

void AppFrame::PrevTab()
{
    notebook->AdvanceSelection(false);
}

void AppFrame::CloseTab()
{
    CloseTab(notebook->GetSelection());
}

void AppFrame::CloseTab(int tab_index, bool force)
{
    dbgenter("CloseTab(%d)\n", tab_index);
    wxASSERT(tab_index < (int)notebook->GetPageCount());

    if (NoteDialog* dlg = dynamic_cast<NoteDialog*>( notebook->GetPage(tab_index) )) {
        if (dlg->isModified()) {
            if (!force && !Confirm(_("Note was modified.\nClose anyway?")))
                return;
        }
    }

    notebook->DeletePage(tab_index);
}

void AppFrame::OnCloseTab(wxCommandEvent& event)
{
    dbgenter("AppFrame::OnCloseTab()\n");
    CloseTab();
}

void AppFrame::OnCloseAllTabs(wxCommandEvent& event)
{
    while(notebook->GetPageCount() > 0)
        CloseTab();
}

bool AppFrame::DeleteNote(const Note note)
{
    dbgenter("DeleteNote(ID=%d)\n", note.id);

    // are you sure?
    wxString msgbox_text = wxString::Format(_("Are you sure you want to delete this note?\n\t%s"),
                                            note.FormatForDisplay());
    if (!Confirm(msgbox_text))
        return false;

    // warning if note is being edited in one of tabs (and was already modified there).
    for (int i = 0; i < (int)notebook->GetPageCount(); i++) {
        if (NoteDialog* dlg = dynamic_cast<NoteDialog*>( notebook->GetPage(i) )) {
            if (dlg->getID() == note.id && dlg->isModified()) {
                if (!Confirm(_("The note you are about to delete was modified but not saved yet.\n"
                               "Are you sure you want to delete it?")))
                    return false;
            }
        }
    }

    // remove from our edited_notes
    for (int i=0; i<(int)edited_notes.size(); i++) {
        if (edited_notes[i].id == note.id) {
            edited_notes.erase(edited_notes.begin() + i);
            break;
        }
    }

    // delete note from database
    if (!db->DeleteNote(note.id)) {
        ErrMsg(_("Failed to delete note #%u"), note.id);
        return false;
    }

    // notify all NotesPanel about deleted note, and close tab which is editing it
    for (int i=0; i<(int)notebook->GetPageCount(); i++) {
        AppTab *page = dynamic_cast<AppTab*>( notebook->GetPage(i) );
        wxASSERT(page);
        page->NotifyDeletedNote(note);

        // TODO: this could be moved to Notify callback in NoteDialog, if
        // there was some straightforward method for dialog to request closing
        // itself (dialog doesn't know its index number in notebook).
        if (NoteDialog *dlg = dynamic_cast<NoteDialog*>( notebook->GetPage(i))) {
            if (dlg->getID() == note.id)
                CloseTab(i, true);
        }
    }

    return true;
}

bool AppFrame::DeleteKeyword(const Keyword keyword)
{
    dbgenter("DeleteKeyword(ID=%d)\n", keyword.id);

    //are you sure?
    wxString msgbox_text = wxFormat(
            _("Are you sure you want to delete this keyword?\n\n%s"),
            keyword.sortname);
    if (!Confirm(msgbox_text))
        return false;

    // check if user is editing this keyword in one of tabs
    for (int i = 0; i < (int)notebook->GetPageCount(); i++) {
        if (KeywordDialog* dlg = dynamic_cast<KeywordDialog*>( notebook->GetPage(i) )) {
            if (dlg->getID() == keyword.id && dlg->isModified()) {
                if (!Confirm(_("The keyword you are about to delete was modified but not saved yet.\n"
                               "Are you sure you want to delete it?")))
                    return false;
            }
        }
    }

    // remove from keyword list
    keyword_list.RemoveKeyword(keyword.id);

    // delete keyword from database
    if (!db->DeleteKeyword(keyword.id)) {
        ErrMsg(_("Failed to delete keyword #%u"), keyword.id);
        return false;
    }

    // notify all panels about deleted keyword, and close tabs which are editing it
    for (int i=0; i<(int)notebook->GetPageCount(); i++) {
        AppTab* tab = dynamic_cast<AppTab*>( notebook->GetPage(i) );
        tab->NotifyDeletedKeyword(keyword);

        // TODO: move this into notification handler, once the dialog can
        // request closing itself even when it is not focused
        if (0 != dynamic_cast<KeywordDialog*>( notebook->GetPage(i) ))
            CloseTab(i, true);
    }

    return true;
}

void AppFrame::SaveNote(const Note &arg_note)
{
    Note note = arg_note; //make copy of arg so we can get ID in WriteNewNote()

    if (note.id == 0) {
        dbg("Save new note\n");

        // increase counter of new notes (used for tab name generation)
        new_note_counter++;
        // write note to database (this also sets note ID)
        // sets note ID
        if (!db->WriteNewNote(note)) {
            ErrMsg(_("Failed to write new note."));
            return;
        }

        // insert new note to the begginning of list of edited notes
        edited_notes.insert(edited_notes.begin(), note);

        // notify all NotesPanel tabs of new note, upadte number in NoteDialog caption ("New note #%d")
        for (int i=0; i<(int)notebook->GetPageCount(); i++) {
            AppTab *tab = dynamic_cast<AppTab*>( notebook->GetPage(i) );
            tab->NotifyNewNote(note);

            // TODO: move to notify callback, once dialog can request changing
            if (NoteDialog* dlg = dynamic_cast<NoteDialog*>( notebook-> GetPage(i))) {
                if (dlg->getID() == 0)
                    notebook->SetPageText(i, CaptionNewNote());
            }
        }

    } else {    // existing note was changed
        dbg("Changed note ID=%d\n", note.id);

        // write note to database
        if (!db->OverwriteNote(note)) {
            ErrMsg(_("Failed to update note #%u"), note.id);
            return;
        }

        // insert/move to beginning of edited_notes
        for (int i=0; i<(int)edited_notes.size(); i++) {
            if (edited_notes[i].id == note.id) {
                edited_notes.erase(edited_notes.begin() + i);
                break;
            }
        }
        edited_notes.insert(edited_notes.begin(), note);

        // notify all panel tabs of edited note
        for (int i=0; i<(int)notebook->GetPageCount(); i++) {
            AppTab *page = dynamic_cast<AppTab*>( notebook->GetPage(i) );
            page->NotifyEditedNote(note);
        }

    }

}

void AppFrame::OnEditDateFilter(wxCommandEvent& event)
{

    // NotesPanel tab must be active
    int current_tab = notebook->GetSelection();
    NotesPanel* notes_panel;
    if (current_tab != -1)
        notes_panel = dynamic_cast<NotesPanel*>( notebook->GetPage(current_tab) );
    if (current_tab == -1 || !notes_panel) {
        wxBell();
        return;
    }

    // read current filter
    NoteFilter filter;
    notes_panel->GetFilter(filter);

    // check if it is a date filter
    wxString end_date;
    if (!GetEndDate(filter.from_date, &end_date) || filter.to_date != end_date) {
        ErrMsg("Not a date filter");
        return;
    }

    // let user modify filter
    if (!GetNoteFilter(_("Edit date filter."), &filter, FILTERDIALOG_DATE))
        return;

    // set new filter
    dbg("Change filter on tab %d: %s", current_tab, filter.ToString());
    notes_panel->SetFilter(filter);

    // modify page name
    notebook->SetPageText(current_tab, CaptionFromFilter(filter));

    // display number of notes in panel
    wxString status_msg = wxString::Format(_("Found %u notes"), notes_panel->GetItemCount());
    SetStatusText(status_msg);
}

void AppFrame::OnEditFilter(wxCommandEvent& event)
{
    // NotesPanel tab must be active
    int current_tab = notebook->GetSelection();
    NotesPanel* notes_panel;
    if (current_tab != -1)
        notes_panel = dynamic_cast<NotesPanel*>( notebook->GetPage(current_tab) );
    if (current_tab == -1 || !notes_panel) {
        wxBell();
        return;
    }

    // read current filter
    NoteFilter filter;
    notes_panel->GetFilter(filter);

    // let user modify it
    if (!GetNoteFilter(_("Edit note filter."), &filter))
        return;

    // set new filter
    dbg("Change filter on tab %d: keyword='%s', from='%s', to='%s', body='%s'\n",
        current_tab, filter.KeywordsToString(), filter.from_date, filter.to_date, filter.body);
    notes_panel->SetFilter(filter);

    // modify page name
    notebook->SetPageText(current_tab, CaptionFromFilter(filter));

    // display number of notes in panel
    wxString status_msg = wxString::Format(_("Found %u notes"), notes_panel->GetItemCount());
    SetStatusText(status_msg);
}

void AppFrame::OnSettings(wxCommandEvent& event)
{
    dbgenter("AppFrame::OnSettings()\n");
    if (ShowSettingsDialog(this)) {
        for (unsigned i=0; i<notebook->GetPageCount(); i++) {
            dbg("Updating appearance of tab %u\n", i);
            AppTab *page = dynamic_cast<AppTab*>( notebook->GetPage(i) );
            wxASSERT(page);
            page->NotifyChangedAppearance();
        }
    }
}

void AppFrame::OnFindKeywords(wxCommandEvent& event)
{
    AddKeywordsPanel();
}

void AppFrame::AddKeywordsPanel()
{
    dbgenter("AddKeywordsPanel()\n");

    // get filter
    KeywordFilter filter;
    if (!InputString(&filter.name, _("Keyword filter"), _("Input partial keyword name")))
        return;
    if (!filter.name.IsEmpty() && !CheckKeyword(filter.name)) {
        ErrMsg(_("Invalid keyword name:\n%s"), filter.name);
        return;
    }

    // create new panel
    KeywordsPanel *panel = new KeywordsPanel(this, notebook);

    // add it to tab
    wxString name = _("Keywords");
    notebook->AddPage(panel, name, true);

    // set filter
    panel->SetFilter(filter);

    // on wxGTK-2.8, this has to be called from outside of constructor,
    // otherwise all but first instances end up with small width.
//    panel->UpdateDisplay();

}

void AppFrame::OnNewKeyword(wxCommandEvent& event)
{
    NewKeywordDialog();
}

void AppFrame::SaveKeyword(const Keyword& arg_keyword)
{
    Keyword keyword = arg_keyword; //make copy of arg so we can get ID in WriteNewNote()

    if (keyword.id == 0) {
        dbg("Save new keyword\n");

        // increase counter of new notes (used for tab name generation)
        new_keyword_counter++;

        // write keyword to database
        // (this also sets keyword ID)
        if (!db->WriteNewKeyword(keyword))
            return;

        // add it to keyword list
        keyword_list.AddKeyword(keyword);

        // notify all KeywordsPanel of new keyword, update number in KeywordDialog caption ("New note #%d")
        for (int i=0; i<(int)notebook->GetPageCount(); i++) {
            AppTab *page = dynamic_cast<AppTab*>( notebook->GetPage(i) );
            page->NotifyNewKeyword(keyword);

            // TODO
            if (KeywordDialog* dlg = dynamic_cast<KeywordDialog*>( notebook->GetPage(i) )) {
                if (dlg->getID() == 0)
                    notebook->SetPageText(i, wxFormat(_("New keyword %u"), new_keyword_counter));
            }
        }

    } else {    // existing keyword was changed
        dbg("changed keyword #%d\n", keyword.id);

        // update keyword in database
        if (!db->OverwriteKeyword(keyword))
            return;

        // update keyword list
        keyword_list.ReplaceKeyword(keyword.id, keyword);

        // notify all panels of changed keyword
        // notify all KeywordsPanel of new keyword, update number in KeywordDialog caption ("New note #%d")
        for (int i=0; i<(int)notebook->GetPageCount(); i++) {
            AppTab *page = dynamic_cast<AppTab*>( notebook->GetPage(i) );
            wxASSERT(page);
            page->NotifyEditedKeyword(keyword);
        }

    }

    wxString info = wxFormat(_("%d keywords, %d hint variants"), keyword_list.KeywordCount(), keyword_list.HintVariantsCount());
    SetStatusText(info);

}

bool AppFrame::LoadKeywords()
{
    wxASSERT(db->isConnected());

    // read all keywords
    vector<Keyword> keywords;
    if (!db->ReadKeywords(&keywords, KeywordFilter()))
        return false;

    // save them in our list
    wxASSERT(keyword_list.KeywordCount() == 0);
    for (unsigned x=0; x<keywords.size(); x++) {
        keyword_list.AddKeyword(keywords[x]);
    }

    return true;
}

void AppFrame::EditKeywordDialog(const Keyword& keyword)
{
    dbgenter("AppFrame::EditNoteDialog()\n");
    if (!db->isConnected()) {
        ErrMsg(_("Database not connected"));
        return;
    }

    dbg("Edit keyword, ID=%d\n", keyword.id);

    // if keyword is already being edited, just focus that tab
    for (int i = 0; i < (int)notebook->GetPageCount(); i++) {
        if (KeywordDialog* k = dynamic_cast<KeywordDialog*>(notebook->GetPage(i))) {
            if (k->getID() == keyword.id) {
                notebook->SetSelection(i);
                dbg("Already opened in tab %d", i);
                return;
            }
        }
    }

    // create keyword dialog
    KeywordDialog *keyword_dialog = new KeywordDialog(notebook, this, keyword);

    // set caption
    wxString caption = wxFormat(_("Edit keyword #%u"), keyword.id);

    // add it as tab
    notebook->AddPage(keyword_dialog, caption, true);
    keyword_dialog->SetFocus();

}

void AppFrame::OnTabChanged(wxAuiNotebookEvent& event)
{
    dbgenter("AppFrame::OnTabChanged()\n");
    dbg("notebook sel: %d, event old sel: %d, event new sel: %d\n", notebook->GetSelection(), event.GetOldSelection(), event.GetSelection());
}


// Note: the close event upon close button is not sent under wxGTK with some window
// managers (eg. fluxbox). See: http://trac.wxwidgets.org/ticket/11934#comment:6
void AppFrame::OnTabClosed(wxAuiNotebookEvent& event)
{
    dbgenter("AppFrame::OnTabClose()\n");
    dbg("notebook sel: %d, event old sel: %d, event new sel: %d\n", notebook->GetSelection(), event.GetOldSelection(), event.GetSelection());

    // we will request closing ourselves by calling notebook->DeletePage in CloseTab()
    event.Veto();

    CloseTab(event.GetSelection());
}

void AppFrame::KeywordUsed(int id)
{
    keyword_list.KeywordUsed(id);
    SaveKeyword(keyword_list.KeywordById(id));
}

void AppFrame::OnTabDragDone(wxAuiNotebookEvent& event)
{
    dbgenter("AppFrame::OnTabDragDone(tab=%d)\n", event.GetSelection());
}
